//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using EffiProzDB.Lib;


namespace EffiProzDB
{


    // fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443
    // fredt@users 20031012 - patch 1.7.2 - better OUTER JOIN implementation
    // fredt@users 20031026 - patch 1.7.2 - more efficient findfirst - especially for multi-column equijoins
    // implemented optimisations similart to patch 465542 by hjbush@users

    /**
     * This class iterates over table rows to select the rows that fulfil join
     * or other conditions. It uses indexes if they are availabe.
     *
     * Extended _in successive versions of HSQLDB.
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public  class TableFilter
    {

        public const int CONDITION_NONE = -1;     // not a condition expression
        public const int CONDITION_UNORDERED = 0;      // not candidate for eStart or eEnd
        public const int CONDITION_START_END = 1;    // candidate for eStart and eEnd
        public const int CONDITION_START = 2;    // candidate for eStart
        public const int CONDITION_END = 3;    // candidate for eEnd
        public const int CONDITION_OUTER = 4;    // add to this
        public Table filterTable;
        private string tableAlias;
        public HashMappedList<string, Expression> columnAliases;
        public Index filterIndex;
        private Object[] emptyData;
        public bool[] usedColumns;
        private Expression eStart, eEnd;

        //
        public Expression eAnd;

        //
        public bool isOuterJoin;                      // table joined with OUTER JOIN
        public bool isAssigned;                       // conditions have been assigned to this
        public bool isMultiFindFirst;                 // findFirst() uses multi-column index
        public Expression[] findFirstExpressions;             // expressions for column values

        //    //
        private RowIterator it;
        public   Object[]     currentData;
        public Row currentRow;

        //
        public Object[] currentJoinData;

        // addendum to the result of findFirst() and next() with isOuterJoin==true
        // when the result is false, it indicates if a non-join condition caused the failure
        public bool nonJoinIsNull;

        // indicates current data is empty data produced for an outer join
        public bool isCurrentOuter;

        /**
         * Constructor declaration
         *
         *
         * @param t
         * @param alias
         * @param outerjoin
         */
        public TableFilter(Table t, string alias, HashMappedList<string,Expression> columnList,
                    bool outerjoin)
        {

            filterTable = t;
            tableAlias = alias == null ? t.getName().name
                                          : alias;
            columnAliases = columnList;
            isOuterJoin = outerjoin;
            emptyData = filterTable.getEmptyRowData();
            usedColumns = filterTable.getNewColumnCheckList();
        }

        /**
         * Returns the alias or the table name.
         * Never returns null;
         * @return
         */
        public string getName()
        {
            return tableAlias;
        }

        /**
         * Retrieves this object's filter Table object.
         *
         * @return this object's filter Table object
         */
        public Table getTable()
        {
            return filterTable;
        }

            /**
             * Retrieves a CONDITION_XXX code indicating how a condition
             * expression can be used for a TableFilter.
             *
             * @param exprType an expression type code
             * @return
             */
        public static int getConditionType(Expression e)
        {

                int exprType = e.getType();

                switch (exprType) {

                    case Expression.NOT_EQUAL :
                    case Expression.LIKE :
                        return CONDITION_UNORDERED;

                    case Expression.IN : {
                        return e.isQueryCorrelated ? CONDITION_NONE
                                                   : CONDITION_UNORDERED;
                    }
                    case Expression.IS_NULL :
                    case Expression.EQUAL : {
                        return CONDITION_START_END;
                    }
                    case Expression.BIGGER :
                    case Expression.BIGGER_EQUAL : {
                        return CONDITION_START;
                    }
                    case Expression.SMALLER :
                    case Expression.SMALLER_EQUAL : {
                        return CONDITION_END;
                    }
                    default : {

                        // not a condition so forget it
                        return CONDITION_NONE;
                    }
                }
            }

            // TODO: Optimize
            //
            // The current way always chooses eStart, eEnd conditions
            // using first encountered eligible index
            //
            // We should check if current index offers better selectivity/access
            // path than previously assigned iIndex.
            //
            // EXAMPLE 1:
            //
            // CREATE TABLE t (c1 int, c2 int primary key)
            // CREATE INDEX I1 ON t(c1)
            // SELECT
            //      *
            // FROM
            //      t
            // WHERE
            //     c1 = | < | <= | >= | > ...
            // AND
            //     c2 = | < | <= | >= | > ...
            //
            // currently always chooses iIndex / condition (c1/I1), over
            // index / condition (c2/pk), whereas index / condition (c2/pk)
            // may well be better, especially if condition on c2 is equality
            // (condition_start_end) and conditionon(s) on c1 involve range
            // (condition_start, condition_end, or some composite).
            //
            // Currently, the developer/client software must somehow know facts
            // both about the table, the query and the way HSQLDB forms its
            // plans and, based on this knowlege, perhaps decide to reverse
            // order by explicitly issuing instead:
            //
            // SELECT
            //      *
            // FROM
            //      t
            // WHERE
            //     c2 = | < | <= | >= | > ...
            // AND
            //     c1 = | < | <= | >= | > ...
            //
            // to get optimal index choice.
            //
            // The same thing applies to and is even worse for joins.
            //
            // Consider the following (highly artificial, but easy to
            // understand) case:
            //
            // CREATE TABLE T1(ID INTEGER PRIMARY KEY, C1 INTEGER)
            // CREATE INDEX I1 ON T1(C1)
            // CREATE TABLE T2(ID INTEGER PRIMARY KEY, C1 INTEGER)
            // CREATE INDEX I2 ON T2(C1)
            //
            // select * from t1, t2 where t1.c1 = t2.c1 and t1.id = t2.id
            //
            // Consider the worst value distribution where t1 and t2 are both
            // 10,000 rows, c1 selectivity is nil (all values are identical)
            // for both tables, and, say, id values span the range 0..9999
            // for both tables.
            //
            // Then time to completion on 500 MHz Athlon testbed using memory
            // tables is:
            //
            // 10000 row(s) _in 309114 ms
            //
            // whereas for:
            //
            // select * from t1, t2 where t1.id = t2.id and t1.c1 = t2.c1
            //
            // time to completion is:
            //
            // 10000 row(s) _in 471 ms
            //
            // Hence, the unoptimized query takes 656 times as long as the
            // optimized one!!!
            //
            // EXAMPLE 2:
            //
            // If there are, say, two non-unique candidate indexes,
            // and some range or equality predicates against
            // them, preference should be given to the one with
            // better selectivity (if the total row count of the
            // table is large, otherwise the overhead of making
            // the choice is probably large w.r.t. any possible
            // savings).  Might require maintaining some basic
            // statistics or performing appropriate index probes
            // at the time the plan is being generated.

            /**
             * Chooses certain query conditions and assigns a copy of them to this
             * filter. The original condition is set to Expression.TRUE once assigned.
             *
             * @param condition
             *
             * @ 
             */
        public void setConditions(Session session,
                             Expression condition)
        {

            setCondition(session, condition);

            if (filterIndex == null)
            {
                filterIndex = filterTable.getPrimaryIndex();
            }

            if (filterIndex.getVisibleColumns() == 1 || eStart == null
                    || eAnd == null || eStart.exprType != Expression.EQUAL)
            {
                return;
            }

            bool[] check = filterTable.getNewColumnCheckList();
            Expression[] expr = new Expression[check.Length];
            int colindex = eStart.getArg().getColumnNr();

            check[colindex] = true;
            expr[colindex] = eStart.getArg2();

            eAnd.getEquiJoinColumns(this, check, expr);

            if (ArrayUtil.containsAllTrueElements(check, filterIndex.colCheck))
            {
                isMultiFindFirst = true;
                findFirstExpressions = expr;
            }
        }

        private void setCondition(Session session,
                                  Expression e)
        {

            int type = e.getType();
            Expression e1 = e.getArg();
            Expression e2 = e.getArg2();

            isAssigned = true;

            if (type == Expression.AND)
            {
                setCondition(session, e1);
                setCondition(session, e2);

                return;
            }

            if (type == Expression.OR && isOuterJoin && e.isInJoin
                    && e.outerFilter == this)
            {
                addAndCondition(e);
                e.setTrue();

                return;
            }

            int conditionType = getConditionType(e);

            if (conditionType == CONDITION_NONE)
            {

                // not a condition expression
                return;
            }

            // fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443
            if (e1.getFilter() == this && e2.getFilter() == this)
            {
                conditionType = CONDITION_UNORDERED;
            }
            else if (e1.getFilter() == this)
            {
                if (!e.isInJoin && isOuterJoin)
                {

                    // do not use a where condition on the second table _in outer joins
                    return;
                }

                // ok include this
            }
            else if ((e2.getFilter() == this)
                     && (conditionType != CONDITION_UNORDERED))
            {

                // swap and try again to allow index usage
                e.swapCondition();
                setCondition(session, e);

                return;
            }
            else if (e1.outerFilter == this)
            {

                // fredt - this test is last to allow swapping the terms above
                conditionType = CONDITION_OUTER;
            }
            else
            {

                // unrelated: don't include
                return;
            }

            //        Trace.doAssert(e1.getFilter() == this, "setCondition");
            if (!e2.isResolved())
            {
                return;
            }

            // fredt - condition defined _in outer but not this one
            if (e1.outerFilter != null && e1.outerFilter != this)
            {
                return;
            }

            if (conditionType == CONDITION_UNORDERED)
            {
                addAndCondition(e);

                return;
            }

            if (conditionType == CONDITION_OUTER)
            {
                addAndCondition(e);

                return;
            }

            int i = e1.getColumnNr();
            Index index = filterTable.getIndexForColumn(session, i);

            if (index == null || (filterIndex != index && filterIndex != null))
            {
                addAndCondition(e);

                return;
            }

            filterIndex = index;

            switch (conditionType)
            {

                case CONDITION_START_END:
                    {

                        // candidate for both start and end
                        if ((eStart != null) || (eEnd != null))
                        {
                            addAndCondition(e);

                            return;
                        }

                        eStart = new Expression(e);
                        eEnd = eStart;

                        break;
                    }
                case CONDITION_START:
                    {

                        // candidate for start
                        if (eStart != null)
                        {
                            addAndCondition(e);

                            return;
                        }

                        eStart = new Expression(e);

                        break;
                    }
                case CONDITION_END:
                    {

                        // candidate for end
                        if (eEnd != null)
                        {
                            addAndCondition(e);

                            return;
                        }

                        eEnd = new Expression(e);

                        break;
                    }
            }

            e.setTrue();
        }

            /**
             * Finds the first row _in the table (using an index if there is one) and
             * checks it against the eEnd (range) and eAnd (other conditions)
             * Expression objects. (fredt)
             *
             * @return true if first row was found, else false
             */

           public bool findFirst(Session session)   {

                nonJoinIsNull  = false;
                isCurrentOuter = false;

                if (filterIndex == null) {
                    filterIndex = filterTable.getPrimaryIndex();
                }

                if (isMultiFindFirst) {
                    bool convertible = true;
                    int[]   types       = filterTable.getColumnTypes();

                    currentJoinData = filterTable.getEmptyRowData();

                    for (int i = 0; i < findFirstExpressions.Length; i++) {
                        Expression e = findFirstExpressions[i];

                        if (e != null) {
                            Object value = e.getValue(session);

                            if (Column.compareToTypeRange(value, types[i]) != 0) {
                                convertible = false;

                                break;
                            }

                            value = Column.convertObject(value, types[i]);
                            currentJoinData[i] = e.getValue(session, types[i]);
                        }
                    }

                    it = convertible
                         ? filterIndex.findFirstRow(session, currentJoinData)
                         : filterIndex.emptyIterator();

                    if (!it.hasNext()) {
                        ArrayUtil.clearArray(ArrayUtil.CLASS_CODE_OBJECT,
                                             currentJoinData, 0,
                                             currentJoinData.Length);
                    }
                } else if (eStart == null) {
                    it = eEnd == null ? filterIndex.firstRow(session)
                                      : filterIndex.findFirstRowNotNull(session);
                } else {
                    Object value      = eStart.getArg2().getValue(session);
                    int    valuetype  = eStart.getArg2().getDataType();
                    int    targettype = eStart.getArg().getDataType();

                    it = getFirstIterator(session, eStart.getType(), value,
                                          valuetype, filterIndex, targettype);
                }

                while (true) {
                    currentRow = it.next();

                    if (currentRow == null) {
                        break;
                    }

                    currentData = currentRow.getData();

                    if (!(eEnd == null || eEnd.testCondition(session))) {
                        break;
                    }

                    if (eAnd == null || eAnd.testCondition(session)) {
                        return true;
                    }
                }

                currentRow  = null;
                currentData = emptyData;

                return false;
            }

           static RowIterator getFirstIterator(Session session, int eType,
                                               Object value, int valueType,
                                               Index index,
                                               int targetType)
           {

               RowIterator it = null;
               int range = 0;

               if (targetType != valueType)
               {
                   range = Column.compareToTypeRange(value, targetType);
               }

               if (range == 0)
               {
                   value = Column.convertObject(value, targetType);
                   it = index.findFirstRow(session, value, eType);
               }
               else
               {
                   switch (eType)
                   {

                       case Expression.BIGGER_EQUAL:
                       case Expression.BIGGER:
                           if (range < 0)
                           {
                               it = index.findFirstRowNotNull(session);

                               break;
                           }
                           break;
                       default:
                           it = index.emptyIterator();
                           break;
                   }
               }

               return it;
           }

           /**
            * Advances to the next available value. <p>
            *
            * @return true if a next value is available upon exit
            *
            * @  if a database access error occurs
            */
           public bool next(Session session)
           {

               bool result = false;

               nonJoinIsNull = false;
               isCurrentOuter = false;

               while (true)
               {
                   currentRow = it.next();

                   if (currentRow == null)
                   {
                       break;
                   }

                   currentData = currentRow.getData();

                   if (!(eEnd == null || eEnd.testCondition(session)))
                   {
                       break;
                   }

                   if (eAnd == null || eAnd.testCondition(session))
                   {
                       result = true;

                       break;
                   }
               }

               if (result)
               {
                   return true;
               }

               currentRow = null;
               currentData = emptyData;

               return false;
           }

           public bool nextOuter(Session session)
           {

               nonJoinIsNull = false;
               isCurrentOuter = true;
               currentData = emptyData;
               currentRow = null;

               return eAnd == null || (eAnd.getFilter() != this && eAnd.isInJoin)
                      || eAnd.testCondition(session);
           }

           /**
            * Forms a new conjunction using the given condition and this filter's
            * pre-existing AND condition, or sets the given condition as this filter's
            * AND condition when there is no such pre-exisiting object.
            *
            * @param e the condition to add
            */
           private void addAndCondition(Expression e)
           {

               Expression e2 = new Expression(e);

               if (eAnd == null)
               {
                   eAnd = e2;
               }
               else
               {
                   Expression and = new Expression(Expression.AND, eAnd, e2);

                   eAnd = and;
               }

               e.setTrue();
           }

           /**
            * Removes reference to Index to avoid possible memory leaks after alter
            * table or drop index
            */
           public void setAsCheckFilter()
           {
               filterIndex = null;
           }

        // boucheb@users 20030415 - added for debugging support

            /**
             * Retreives a string representation of this obejct. <p>
             *
             * The returned string describes this object's table, alias
             * access mode, index, join mode, Start, End and And conditions.
             *
             * @return a string representation of this object
             */
            public string describe(Session session) {

                StringBuilder sb;
                string       temp;
                Index        index;
                Index        primaryIndex;
                int[]        primaryKey;
                bool      hidden;
                bool      fullScan;

                sb           = new StringBuilder();
                index        = filterIndex;
                primaryIndex = filterTable.getPrimaryIndex();
                primaryKey   = filterTable.getPrimaryKey();
                hidden       = false;
                fullScan     = (eStart == null && eEnd == null);

                if (index == null) {
                    index = primaryIndex;
                }

                if (index == primaryIndex && primaryKey.Length == 0) {
                    hidden   = true;
                    fullScan = true;
                }

                sb.Append(base.ToString()).Append('\n');
                sb.Append("table=[").Append(filterTable.getName().name).Append("]\n");
                sb.Append("alias=[").Append(tableAlias).Append("]\n");
                sb.Append("access=[").Append(fullScan ? "FULL SCAN"
                                                      : "INDEX PRED").Append("]\n");
                sb.Append("index=[");
                sb.Append(index == null ? "NONE"
                                        : index.getName() == null ? "UNNAMED"
                                                                  : index.getName()
                                                                  .name);
                sb.Append(hidden ? "[HIDDEN]]\n"
                                 : "]\n");
                sb.Append("isOuterJoin=[").Append(isOuterJoin).Append("]\n");

                temp = eStart == null ? "null"
                                      : eStart.describe(session);

                sb.Append("eStart=[").Append(temp).Append("]\n");

                temp = eEnd == null ? "null"
                                    : eEnd.describe(session);

                sb.Append("eEnd=[").Append(temp).Append("]\n");

                temp = eAnd == null ? "null"
                                    : eAnd.describe(session);

                sb.Append("eAnd=[").Append(temp).Append("]");

                return sb.ToString();
            }
    }
}
